//
//  main.m
//  glfwdemo006-buffer
//
//  Created by 王洪飞 on 2024/6/10.
//

#import <Foundation/Foundation.h>
#include <glad/glad.h>
#import "glfw3.h"
#include "shader_s.hpp"
#include "stb_image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#import "Camera.hpp"

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
unsigned int loadTexture(const char *path);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

// lighting
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

void framebuffer_size_callback(GLFWwindow *window, int width, int height){
    glViewport(0,0,width, height);
}
void processInput(GLFWwindow *window){
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
            camera.ProcessKeyboard(FORWARD, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
            camera.ProcessKeyboard(BACKWARD, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
            camera.ProcessKeyboard(LEFT, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
            camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
    float xpos = static_cast<float>(xposIn);
    float ypos = static_cast<float>(yposIn);

    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(static_cast<float>(yoffset));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        glfwInit();
//        主版本号
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
//        次版本号
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        
//        创建一个窗口对象,这个窗口存放所有和窗口相关的数据,而且被glfw的其他函数频繁的用到
        GLFWwindow *window = glfwCreateWindow(800, 600, "learn opengl", NULL, NULL);
        if (window == NULL) {
            std::cout << "failed to create glfw window" << std::endl;
            glfwTerminate();
            return  -1;
        }
        glfwMakeContextCurrent(window);
        glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
        glfwSetCursorPosCallback(window, mouse_callback);
        glfwSetScrollCallback(window, scroll_callback);
//        glad是用来管理OpenGL的函数指针的,所有调用任何OpenGL的函数之前先初始化glad
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            std::cout << "failed to initialize glad" << std::endl;
            return -1;
        }
        // configure global opengl state
          // -----------------------------
          glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LESS);
 
        glEnable(GL_STENCIL_TEST);
           glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
           glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

        Shader shader("/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/colorv.vs", "/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/colorf.fs"); // you
        
        
        Shader shaderSingleColor("/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/3.3.shader.vs", "/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/3.3.shader.fs"); // you
        
        // set up vertex data (and buffer(s)) and configure vertex attributes
            // ------------------------------------------------------------------
            float cubeVertices[] = {
                // positions          // texture Coords
                -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
                 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
                 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
                 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
                -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
                -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

                -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
                 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
                 0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
                 0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
                -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
                -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

                -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
                -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
                -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
                -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
                -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
                -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

                 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
                 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
                 0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
                 0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
                 0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
                 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

                -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
                 0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
                 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
                 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
                -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
                -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

                -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
                 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
                 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
                 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
                -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
                -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
            };
            float planeVertices[] = {
                // positions          // texture Coords (note we set these higher than 1 (together with GL_REPEAT as texture wrapping mode). this will cause the floor texture to repeat)
                 5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
                -5.0f, -0.5f,  5.0f,  0.0f, 0.0f,
                -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,

                 5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
                -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,
                 5.0f, -0.5f, -5.0f,  2.0f, 2.0f
            };
        // cube VAO
            unsigned int cubeVAO, cubeVBO;
            glGenVertexArrays(1, &cubeVAO);
            glGenBuffers(1, &cubeVBO);
            glBindVertexArray(cubeVAO);
            glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
            glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
            glBindVertexArray(0);
            // plane VAO
            unsigned int planeVAO, planeVBO;
            glGenVertexArrays(1, &planeVAO);
            glGenBuffers(1, &planeVBO);
            glBindVertexArray(planeVAO);
            glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
            glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), &planeVertices, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
            glBindVertexArray(0);

        // load textures
            // -------------
            unsigned int cubeTexture = loadTexture("/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/wall.jpg");
            unsigned int floorTexture = loadTexture("/Users/wanghongfei/Documents/opengles-demo/glfwdemo006-buffer/glfwdemo006-buffer/awesomeface.png");
        
        // shader configuration
            // --------------------
            shader.use();
            shader.setInt("texture1", 0);
        
        while (!glfwWindowShouldClose(window)) {
            // per-frame time logic
                   // --------------------
           float currentFrame = static_cast<float>(glfwGetTime());
           deltaTime = currentFrame - lastFrame;
           lastFrame = currentFrame;
            
            // input
            processInput(window);
            
            glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
            
            // set uniforms
            shaderSingleColor.use();
            glm::mat4 model = glm::mat4(1.0f);
            glm::mat4 view = camera.GetViewMatrix();
            glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
            shaderSingleColor.setMat4("view", view);
            shaderSingleColor.setMat4("projection", projection);

            shader.use();
            shader.setMat4("view", view);
            shader.setMat4("projection", projection);

            // draw floor as normal, but don't write the floor to the stencil buffer, we only care about the containers. We set its mask to 0x00 to not write to the stencil buffer.
            glStencilMask(0x00);
            // floor
            glBindVertexArray(planeVAO);
            glBindTexture(GL_TEXTURE_2D, floorTexture);
            shader.setMat4("model", glm::mat4(1.0f));
            glDrawArrays(GL_TRIANGLES, 0, 6);
            glBindVertexArray(0);
            
            // 1st. render pass, draw objects as normal, writing to the stencil buffer
                   // --------------------------------------------------------------------
                   glStencilFunc(GL_ALWAYS, 1, 0xFF);
                   glStencilMask(0xFF);
                   // cubes
                   glBindVertexArray(cubeVAO);
                   glActiveTexture(GL_TEXTURE0);
                   glBindTexture(GL_TEXTURE_2D, cubeTexture);
                   model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
                   shader.setMat4("model", model);
                   glDrawArrays(GL_TRIANGLES, 0, 36);
                   model = glm::mat4(1.0f);
                   model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
                   shader.setMat4("model", model);
                   glDrawArrays(GL_TRIANGLES, 0, 36);

                   // 2nd. render pass: now draw slightly scaled versions of the objects, this time disabling stencil writing.
                   // Because the stencil buffer is now filled with several 1s. The parts of the buffer that are 1 are not drawn, thus only drawing
                   // the objects' size differences, making it look like borders.
                   // -----------------------------------------------------------------------------------------------------------------------------
                   glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
                   glStencilMask(0x00);
                   glDisable(GL_DEPTH_TEST);
                   shaderSingleColor.use();
                   float scale = 1.1f;
                   // cubes
                   glBindVertexArray(cubeVAO);
                   glBindTexture(GL_TEXTURE_2D, cubeTexture);
                   model = glm::mat4(1.0f);
                   model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
                   model = glm::scale(model, glm::vec3(scale, scale, scale));
                   shaderSingleColor.setMat4("model", model);
                   glDrawArrays(GL_TRIANGLES, 0, 36);
                   model = glm::mat4(1.0f);
                   model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
                   model = glm::scale(model, glm::vec3(scale, scale, scale));
                   shaderSingleColor.setMat4("model", model);
                   glDrawArrays(GL_TRIANGLES, 0, 36);
                   glBindVertexArray(0);
                   glStencilMask(0xFF);
                   glStencilFunc(GL_ALWAYS, 0, 0xFF);
                   glEnable(GL_DEPTH_TEST);
            
            glfwPollEvents();
            glfwSwapBuffers(window);
        }
    }
    return 0;
}

// utility function for loading a 2D texture from file
// ---------------------------------------------------
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}
